/*
 *  YouTube Picker
 * 
 *  Copyright (c) 2014 Ted Hive <tedhive@gmail.com>
 *  
 *  Developed from YTD2 [ http://sourceforge.net/projects/ytd2/ ]
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * version 3.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package com.googlecode.youtubepicker;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.protocol.HttpContext;
import org.apache.log4j.Logger;
import org.eclipse.core.runtime.IProgressMonitor;

public class Download {

    private static Logger logger = Logger.getLogger(Download.class);
	
    private IProgressMonitor monitor;
	private String videoId;
	private String tagDir;
	private String quality;

	private Map<String, String> mapExt = new HashMap<String, String>();
	
	private static final String szYTHOSTREGEX = "^((H|h)(T|t)(T|t)(P|p)(S|s)?://)?(.*)\\.((Y|y)(O|o)(U|u)(T|t)(U|u)(B|b)(E|e)\\..{2,5}|(G|g)(O|o)(O|o)(G|g)(L|l)(E|e)(V|v)(I|i)(D|d)(E|e)(O|o)\\..{2,5})/";
	
	public static final String[] RESOLUTIONS = new String[] { "3D.mpeg.HD", "3D.webm.HD", "mpeg.HD.full", "mpeg.HD.half", "webm.HD.full", "webm.HD.half", "3D.mpeg.SD", "3D.webm.SD", "mpeg.SD", "webm.SD.full", "webm.SD.half", "flv.SD.full", "flv.SD.half", "mpeg.LD.full", "mpeg.LD.half", "flv.LD" };
	
	public Download(IProgressMonitor monitor, String videoId, String tagDir, String quality) {
    	this.monitor = monitor;
		this.videoId = videoId;
		this.tagDir = tagDir;
		this.quality = quality;
		
		mapExt.put("3D.mpeg.HD", "(3D MPEG HD).mp4");

		mapExt.put("3D.webm.HD", "(3D WEBM HD).webm");
		
		mapExt.put("mpeg.HD.full", "(MPEG Full HD).mp4");
		mapExt.put("mpeg.HD.half", "(MPEG Half HD).mp4");
		
		mapExt.put("webm.HD.full", "(WEBM Full HD).webm");
		mapExt.put("webm.HD.half", "(WEBM Half HD).webm");
		
		mapExt.put("3D.mpeg.SD", "(3D MPEG SD).mp4");

		mapExt.put("3D.webm.SD", "(3D WEBM SD).webm");

		mapExt.put("mpeg.SD", "(MPEG SD).mp4");
		
		mapExt.put("webm.SD.full", "(WEBM Full SD).webm");
		mapExt.put("webm.SD.half", "(WEBM Half SD).webm");
		
		mapExt.put("flv.SD.full", "(FLV Full SD).flv");
		mapExt.put("flv.SD.half", "(FLV Half SD).flv");
		
		mapExt.put("mpeg.LD.full", "(MPEG Full LD).mp4");
		mapExt.put("mpeg.LD.half", "(MPEG Half LD).mp4");
		
		mapExt.put("flv.LD", "(FLV LD).flv");
	}

    public void run() throws Exception {
    	monitor.beginTask("Start download video [ " + videoId + " ]", 1);
    	
    	Map<String, Object> data = grabVideo("http://www.youtube.com/watch?v=" + videoId);
    	String title = data.get("title") + "";
    	Map<String, YTURL> videos = (Map<String, YTURL>)data.get("video");
    	String videoUrl = "";
    	
    	if (quality.length() > 0) {
        	for (int i = 0; i < Download.RESOLUTIONS.length; i++) {
        		String key = Download.RESOLUTIONS[i];
        		if (key.equalsIgnoreCase(quality)) {
            		if (videos.containsKey(key)) {
            			YTURL yu = videos.get(key);
            			if (yu.getsURL() != null) {
            				videoUrl = yu.getsURL();
            				break;
            			}
            		}
        		}
        	}
    	}
		if (videoUrl.length() == 0) {
			quality = "";
	    	for (int i = 0; i < Download.RESOLUTIONS.length; i++) {
	    		String key = Download.RESOLUTIONS[i];
	    		if (videos.containsKey(key)) {
	    			YTURL yu = videos.get(key);
	    			if (yu.getsURL() != null) {
        				videoUrl = yu.getsURL();
        				quality = key;
        				break;
	    			}
	    		}
	    	}
		}
		if (videoUrl.length() == 0) {
			throw new Exception("Video not found!");
		}
		
		int no = 0;
		File file = new File(tagDir, title + " " + mapExt.get(quality));
		while (file.exists()) {
			no++;
			file = new File(tagDir, title + " [" + no + "] " + mapExt.get(quality));
		}
		
		download(videoUrl, file.getAbsolutePath());
    	
    	monitor.worked(1);
    }
	
    private void download(String sURL, String filename) throws Exception {
		HttpClient httpclient = new DefaultHttpClient();
		httpclient.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BEST_MATCH);
		HttpGet httpget = new HttpGet( getURI(sURL) );
		HttpHost target = null;
		HttpContext localContext = null;
		if (sURL.toLowerCase().startsWith("https")) {
			target = new HttpHost( getHost(sURL), 443, "https" );
		} else {
			target = new HttpHost( getHost(sURL), 80, "http" );
		}
		HttpResponse response = httpclient.execute(target, httpget, localContext);

		boolean rc = false;
		boolean rc204 = false;
		boolean rc302 = false;

		if (!(rc = response.getStatusLine().toString().toLowerCase().matches("^(http)(.*)200(.*)")) & 
				!(rc204 = response.getStatusLine().toString().toLowerCase().matches("^(http)(.*)204(.*)")) &
				!(rc302 = response.getStatusLine().toString().toLowerCase().matches("^(http)(.*)302(.*)"))) {
	       	httpclient.getConnectionManager().shutdown();
			return;
		}
		if (rc204) {
	       	httpclient.getConnectionManager().shutdown();
			return;
		}

		BufferedInputStream binaryreader = null;
		HttpEntity entity = response.getEntity();
		
        if (entity != null) {
			if (response.getFirstHeader("Content-Type").getValue().toLowerCase().matches("^text/html(.*)")) {
		       	httpclient.getConnectionManager().shutdown();
		       	throw new Exception("Video not found");
			} else {
            	String sContentType = response.getFirstHeader("Content-Type").getValue().toLowerCase();
            	if (sContentType.matches("video/(.)*")) {
    				binaryreader = new BufferedInputStream( entity.getContent());
            	} else {
    		       	httpclient.getConnectionManager().shutdown();
    		       	throw new Exception("Video not found");
            	}
			}
        } else {
	       	httpclient.getConnectionManager().shutdown();
	       	throw new Exception("Video not found");
        }
        
		Long iBytesReadSum = (long) 0;
		Long iPercentage = (long) -1;
		Long iBytesMax = Long.parseLong(response.getFirstHeader("Content-Length").getValue());
		FileOutputStream fos = new FileOutputStream(new File(filename));
	    
		byte[] bytes = new byte[4096];
		Integer iBytesRead = 1;
		
		// adjust blocks of percentage to output - larger files are shown with smaller pieces
		Integer iblocks = 10; if (iBytesMax>20*1024*1024) iblocks=4; if (iBytesMax>32*1024*1024) iblocks=2; if (iBytesMax>56*1024*1024) iblocks=1;
		while (iBytesRead > 0) {
			if (monitor.isCanceled()) throw new Exception("Download is cancelled");
			iBytesRead = binaryreader.read(bytes);
			iBytesReadSum += iBytesRead;
			// drop a line every x% of the download 
			if ( (((iBytesReadSum*100/iBytesMax) / iblocks) * iblocks) > iPercentage ) {
				iPercentage = (((iBytesReadSum*100/iBytesMax) / iblocks) * iblocks);
				monitor.subTask("".concat(Long.toString(iPercentage).concat(" % downloaded")));
			}
			// TODO calculate and show ETA for bigger downloads (remaining time > 60s) - every 20%?
			
			try {fos.write(bytes,0,iBytesRead);} catch (IndexOutOfBoundsException ioob) {}
		} // while
    }
    
    public static Map<String, Object> grabVideo(String sURL) {
    	Map<String, Object> tag = new HashMap<String, Object>();
    	tag.put("video", new HashMap<String, YTURL>());
    	tag.put("title", "");
    	
    	try {
    		HttpClient httpclient = new DefaultHttpClient();
    		httpclient.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BEST_MATCH);
    		HttpGet httpget = new HttpGet( getURI(sURL) );
    		HttpHost target = null;
    		HttpContext localContext = null;
			if (sURL.toLowerCase().startsWith("https")) {
				target = new HttpHost( getHost(sURL), 443, "https" );
			} else {
				target = new HttpHost( getHost(sURL), 80, "http" );
			}
			HttpResponse response = httpclient.execute(target, httpget, localContext);

			boolean rc = false;
			boolean rc204 = false;
			boolean rc302 = false;

			if (!(rc = response.getStatusLine().toString().toLowerCase().matches("^(http)(.*)200(.*)")) & 
					!(rc204 = response.getStatusLine().toString().toLowerCase().matches("^(http)(.*)204(.*)")) &
					!(rc302 = response.getStatusLine().toString().toLowerCase().matches("^(http)(.*)302(.*)"))) {
		       	httpclient.getConnectionManager().shutdown();
				return tag;
			}
			if (rc204) {
		       	httpclient.getConnectionManager().shutdown();
				return tag;
			}

			BufferedReader textreader = null;
			HttpEntity entity = response.getEntity();
			
	        if (entity != null) {
				if (response.getFirstHeader("Content-Type").getValue().toLowerCase().matches("^text/html(.*)")) {
					textreader = new BufferedReader(new InputStreamReader(entity.getContent()));
				} else {
			       	httpclient.getConnectionManager().shutdown();
					return tag;
				}
	        } else {
		       	httpclient.getConnectionManager().shutdown();
	        	return tag;
	        }

			String sline = textreader.readLine();
			while (sline != null) {
				if (sline.matches("(.*)\"url_encoded_fmt_stream_map\":(.*)")) {
    				HashMap<String, String> ssourcecodevideourls = new HashMap<String, String>();
					sline = sline.replaceFirst(".*\"url_encoded_fmt_stream_map\": \"", "");
					sline = sline.replaceFirst("\".*", "");
					sline = sline.replace("%25","%");
					sline = sline.replace("\\u0026", "&");
					sline = sline.replace("\\", "");
					String[] ssourcecodeyturls = sline.split(",");
					String sResolutions = "found video URL for resolution: ";
					
					for (String urlString : ssourcecodeyturls) {
						// assuming rtmpe is used for all resolutions, if found once - end download
						if (urlString.matches(".*conn=rtmpe.*")) {
							logger.debug("RTMPE found. cannot download this one!");
							logger.debug("Unable to download video due to unsupported protocol (RTMPE).");
							break;
						}
						String[] fmtUrlPair = urlString.split("url=http",2);
						fmtUrlPair[1] = "url=http"+fmtUrlPair[1]+"&"+fmtUrlPair[0];
						// grep itag=xz out and use xy as hash key
						// 2013-02 itag now has up to 3 digits
						fmtUrlPair[0] = fmtUrlPair[1].substring(fmtUrlPair[1].indexOf("itag=")+5, fmtUrlPair[1].indexOf("itag=")+5+1+(fmtUrlPair[1].matches(".*itag=[0-9]{2}.*")?1:0)+(fmtUrlPair[1].matches(".*itag=[0-9]{3}.*")?1:0));
						fmtUrlPair[1] = fmtUrlPair[1].replaceFirst("url=http%3A%2F%2F", "http://");
						fmtUrlPair[1] = fmtUrlPair[1].replaceAll("%3F","?").replaceAll("%2F", "/").replaceAll("%3B",";").replaceAll("%2C",",").replaceAll("%3D","=").replaceAll("%26", "&").replaceAll("%252C", "%2C").replaceAll("sig=", "signature=").replaceAll("&s=", "&signature=").replaceAll("\\?s=", "?signature=");
						
						// remove duplicated &itag=xy
						if (StringUtils.countMatches(fmtUrlPair[1], "itag=")==2)
							fmtUrlPair[1] = fmtUrlPair[1].replaceFirst("itag=[0-9]{1,3}", "");
						
						try {
							ssourcecodevideourls.put(fmtUrlPair[0], fmtUrlPair[1]); // save that URL
							//logger.info(String.format( "video url saved with key %s: %s", fmtUrlPair[0], ssourcecodevideourls.get(fmtUrlPair[0]) ));
							sResolutions = sResolutions.concat(
									fmtUrlPair[0].equals( "37")?"1080p mpeg, ":		// HD		type=video/mp4;+codecs="avc1.64001F,+mp4a.40.2"
									fmtUrlPair[0].equals( "22")?"720p mpeg, ":		// HD		type=video/mp4;+codecs="avc1.64001F,+mp4a.40.2"
									fmtUrlPair[0].equals( "84")?"1080p 3d mpeg, ":	// HD 3D	type=video/mp4;+codecs="avc1.64001F,+mp4a.40.2"
									fmtUrlPair[0].equals( "35")?"480p flv, ":		// SD		type=video/x-flv
									fmtUrlPair[0].equals( "18")?"360p mpeg, ":		// SD		type=video/mp4;+codecs="avc1.42001E,+mp4a.40.2"
									fmtUrlPair[0].equals( "34")?"360p flv, ":		// SD		type=video/x-flv
									fmtUrlPair[0].equals( "82")?"360p 3d mpeg, ":	// SD 3D	type=video/mp4;+codecs="avc1.42001E,+mp4a.40.2"
									fmtUrlPair[0].equals( "36")?"240p mpeg 3gpp, ":	// LD		type=video/3gpp;+codecs="mp4v.20.3,+mp4a.40.2"
									fmtUrlPair[0].equals( "17")?"114p mpeg 3gpp, ":	// LD		type=video/3gpp;+codecs="mp4v.20.3,+mp4a.40.2"
										
									fmtUrlPair[0].equals( "46")?"1080p webm, ":		// HD 		type=video/webm;+codecs="vp8.0,+vorbis"&
									fmtUrlPair[0].equals( "45")?"720p webm, ":		// HD		type=video/webm;+codecs="vp8.0,+vorbis"
									fmtUrlPair[0].equals("100")?"1080p 3d webm, ":	// HD 3D	type=video/webm;+codecs="vp8.0,+vorbis"&
									fmtUrlPair[0].equals( "44")?"480p webm, ":		// SD		type=video/webm;+codecs="vp8.0,+vorbis"
									fmtUrlPair[0].equals( "43")?"360p webm, ":		// SD		type=video/webm;+codecs="vp8.0,+vorbis"
									fmtUrlPair[0].equals("102")?"360p 3d webm, ":	// SD 3D	type=video/webm;+codecs="vp8.0,+vorbis"&						
									fmtUrlPair[0].equals(  "5")?"240p flv, ":		// LD		type=video/x-flv
									"unknown resolution! (".concat(fmtUrlPair[0]).concat(")"));
						} catch (java.lang.ArrayIndexOutOfBoundsException aioobe) {
						}
					} // for
					
					Map<String, YTURL> videos = new HashMap<String, YTURL>();

					videos.put("3D.mpeg.HD", new YTURL(ssourcecodevideourls.get("84"), sURL, "3D"));
					videos.put("mpeg.HD.full", new YTURL(ssourcecodevideourls.get("37"), sURL));
					videos.put("mpeg.HD.half", new YTURL(ssourcecodevideourls.get("22"), sURL));
					videos.put("3D.webm.HD", new YTURL(ssourcecodevideourls.get("100"), sURL, "3D"));
					videos.put("webm.HD.full", new YTURL(ssourcecodevideourls.get("46"), sURL));
					videos.put("webm.HD.half", new YTURL(ssourcecodevideourls.get("45"), sURL));
					videos.put("3D.webm.SD", new YTURL(ssourcecodevideourls.get("102"), sURL, "3D"));
					videos.put("webm.SD.full", new YTURL(ssourcecodevideourls.get("44"), sURL));
					videos.put("webm.SD.half", new YTURL(ssourcecodevideourls.get("43"), sURL));
					videos.put("flv.SD.full", new YTURL(ssourcecodevideourls.get("35"), sURL));
					videos.put("flv.SD.half", new YTURL(ssourcecodevideourls.get("34"), sURL));
					videos.put("3D.mpeg.SD", new YTURL(ssourcecodevideourls.get("82"), sURL, "3D"));
					videos.put("mpeg.SD", new YTURL(ssourcecodevideourls.get("18"), sURL));
					videos.put("mpeg.LD.full", new YTURL(ssourcecodevideourls.get("36"), sURL));
					videos.put("mpeg.LD.half", new YTURL(ssourcecodevideourls.get("17"), sURL));
					videos.put("flv.LD", new YTURL(ssourcecodevideourls.get( "5"), sURL));
					
					tag.put("video", videos);
				}
				
				if (sline.matches("(.*)<meta name=\"title\" content=(.*)")) {
					String stmp = sline.replaceFirst("(.*)<meta name=\"title\" content=", "").trim();
					// change html characters to their UTF8 counterpart
					stmp = UTF8.changeHTMLtoUTF8(stmp);
					stmp = stmp.replaceFirst("^\"", "").replaceFirst("\">$", "");
					
					// http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
					// 
					stmp = stmp.replaceAll("<", "");
					stmp = stmp.replaceAll(">", "");
					stmp = stmp.replaceAll(":", "");
					stmp = stmp.replaceAll("/", " ");
					stmp = stmp.replaceAll("\\\\", " ");
					stmp = stmp.replaceAll("|", "");
					stmp = stmp.replaceAll("\\?", "");
					stmp = stmp.replaceAll("\\*", "");
					stmp = stmp.replaceAll("/", " ");
					stmp = stmp.replaceAll("\"", " ");
					stmp = stmp.replaceAll("%", "");
					
					tag.put("title", stmp);
				}
				sline = textreader.readLine();
			} // while
    	} catch (Exception e) {
    		logger.error("", e);
    	}
    	
    	return tag;
    }
    
	private static String getURI(String sURL) {
		String suri = "/".concat(sURL.replaceFirst(szYTHOSTREGEX, ""));
		return(suri);
	}

	private static String getHost(String sURL) {
		String shost = sURL.replaceFirst(szYTHOSTREGEX, "");
		shost = sURL.substring(0, sURL.length()-shost.length());
		shost = shost.toLowerCase().replaceFirst("http[s]?://", "").replaceAll("/", "");
		return(shost);
	}
	
}
